package org.go.scheduler;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathExpressionException;

import org.go.domain.GoDomain;
import org.go.ee.jmx.middle.localhost.RmiAgent;
import org.go.expcetion.GoException;
import org.go.expcetion.SchedulerConfigException;
import org.go.expcetion.SchedulerException;
import org.go.expcetion.ValidationException;
import org.go.log.Log;
import org.go.log.LoggerFactory;
import org.go.plugin.GoPlugin;
import org.go.scheduler.memory.MemoryScheduler;
import org.go.scheduler.rmi.RMIClientScheduler;
import org.go.scheduler.rmi.RemoteScheduler;
import org.go.scheduler.zk.ZkScheduler;
import org.go.scheduler.zk.util.ZKManager;
import org.go.threadpool.GoThreadPool;
import org.go.threadpool.GoThreadPoolRepository;
import org.go.util.ClassUtils;
import org.go.util.PropertiesParser;
import org.go.util.XmlUtil;
import org.go.workstore.memory.WorkStoreMemory;
import org.go.workstore.zk.WorkStoreZK;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * 
 * @author hejie
 *
 */
public class StdSchedulerFactory implements SchedulerFactory {
	private static final Log log = LoggerFactory.getLog(StdSchedulerFactory.class);
	private PropertiesParser cfg;
	Document goConfigDoc = null;

	public StdSchedulerFactory() {

	}

	/**
	 * Create a StdSchedulerFactory that has been initialized via
	 * <code>{@link #initialize(String)}</code>.
	 *
	 * @see #initialize(String)
	 */
	public StdSchedulerFactory(String fileName) throws Exception {
		initXmlFile(fileName);
		instantiate();
	}

	private String getSchedulerName() {
		return cfg.getStringProperty(GoDomain.PROP_SCHED_INSTANCE_NAME, "QuartzScheduler");
	}

	private void initXmlFile() throws ValidationException, SAXException, IOException, ClassNotFoundException, ParseException, XPathException, Exception {
		for (String path : GoDomain.GO_CONFIG_PATH) {
			Document doc = XmlUtil.processFile(path, path);
			if (doc != null) {
				goConfigDoc = doc;
				instantiate();
			}
		}
	}

	private void initXmlFile(String propFileName) throws ValidationException, SAXException, IOException, ClassNotFoundException, ParseException, XPathException, Exception {
		File propFile = new File(propFileName);
		if (propFile.exists()) {
			InputStream in = new BufferedInputStream(new FileInputStream(propFileName));
			Document doc = XmlUtil.readXML(in);
			goConfigDoc = doc;
		} else {
			InputStream is = XmlUtil.getInputStream(propFileName);
			if (is == null) {
				log.error("config file not exist");
				throw new Exception("config file not exist");
			} else {
				InputStream in = new BufferedInputStream(is);
				Document doc = XmlUtil.readXML(in);
				goConfigDoc = doc;
			}
		}
	}

	/**
	 * 
	 * @return
	 * @throws GoException
	 */
	private Collection<Scheduler> instantiate() throws GoException {
		instantiateThreadPool();
		Collection<Scheduler> list = new ArrayList<Scheduler>();
		Collection<Scheduler> rmiClients = instantiateRmiClient();
		if (rmiClients != null) {
			list.addAll(rmiClients);
		}
		Collection<Scheduler> rmiServer = instantiateRmiServer();
		if (rmiServer != null) {
			list.addAll(rmiServer);
		}
		Collection<Scheduler> localeShceds = instantiateLocaleShced();
		if (localeShceds != null) {
			list.addAll(localeShceds);
		}
		Collection<Scheduler> zkShceds = instantiateZKShced();
		if (zkShceds != null) {
			list.addAll(zkShceds);
		}
		initJMX();
		instantiateplugins();
		return list;
	}

	private Collection<Scheduler> instantiateZKShced() throws GoException {
		XPath xpath = XmlUtil.buildXpath();
		try {
			NodeList nodes = (NodeList) xpath.evaluate("/go/schedules/schedule", goConfigDoc, XPathConstants.NODESET);
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = nodes.item(i);
				String id = XmlUtil.getTrimmedToNullString(xpath, "id", node);
				String type = XmlUtil.getTrimmedToNullString(xpath, "type", node);
				if ("ZK".equals(type)) {
					SchedulerResources res = new SchedulerResources();
					res.setWorkerStore(new WorkStoreZK());
					res.addGoPool(GoThreadPoolRepository.getInstance().lookup());
					Properties p = new Properties();
					p.setProperty(ZKManager.keys.zkConnectString.toString(), XmlUtil.getTrimmedToNullString(xpath, ZKManager.keys.zkConnectString.toString(), node));
					p.setProperty(ZKManager.keys.rootPath.toString(), XmlUtil.getTrimmedToNullString(xpath, ZKManager.keys.rootPath.toString(), node));
					p.setProperty(ZKManager.keys.userName.toString(), XmlUtil.getTrimmedToNullString(xpath, ZKManager.keys.userName.toString(), node));
					p.setProperty(ZKManager.keys.password.toString(), XmlUtil.getTrimmedToNullString(xpath, ZKManager.keys.password.toString(), node));
					p.setProperty(ZKManager.keys.zkSessionTimeout.toString(), XmlUtil.getTrimmedToNullString(xpath, ZKManager.keys.zkSessionTimeout.toString(), node));
					Scheduler sched = new ZkScheduler(res, new ZKManager(p));
					res.setName(id);
					SchedulerRepository schedRep = SchedulerRepository.getInstance();
					schedRep.bind(sched);
				}
			}
		} catch (Exception e) {
			throw new GoException("Unable to instantiate class load helper class: " + e.getMessage(), e);
		}
		return null;
	}

	private Collection<Scheduler> instantiateLocaleShced() throws GoException {
		XPath xpath = XmlUtil.buildXpath();
		try {
			NodeList nodes = (NodeList) xpath.evaluate("/go/schedules/schedule", goConfigDoc, XPathConstants.NODESET);
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = nodes.item(i);
				String id = XmlUtil.getTrimmedToNullString(xpath, "id", node);
				String type = XmlUtil.getTrimmedToNullString(xpath, "type", node);
				//String poolid = XmlUtil.getTrimmedToNullString(xpath, "poolid", node);
				if ("LOCAL".equals(type)) {
					SchedulerResources res = new SchedulerResources();
					res.setWorkerStore(new WorkStoreMemory());
					res.addGoPool(GoThreadPoolRepository.getInstance().lookup());
					Scheduler sched = new MemoryScheduler(res);
					res.setName(id);
					SchedulerRepository schedRep = SchedulerRepository.getInstance();
					schedRep.bind(sched);
				}
			}
		} catch (XPathExpressionException e) {
			throw new GoException("Unable to instantiate class load helper class: " + e.getMessage(), e);
		} catch (Exception e) {
			throw new GoException("Unable to instantiate class load helper class: " + e.getMessage(), e);
		}
		return null;
	}

	private void instantiateplugins() throws GoException {
		XPath xpath = XmlUtil.buildXpath();
		try {
			NodeList nodes = (NodeList) xpath.evaluate("/go/plugins/plugin", goConfigDoc, XPathConstants.NODESET);
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = nodes.item(i);
				String id = XmlUtil.getTrimmedToNullString(xpath, "id", node);
				String className = XmlUtil.getTrimmedToNullString(xpath, "class", node);
				String scheduleid = XmlUtil.getTrimmedToNullString(xpath, "scheduleid", node);
				Class<GoPlugin> pool = (Class<GoPlugin>) Class.forName(className);
				GoPlugin plugin = pool.newInstance();
				Scheduler scheduler = SchedulerRepository.getInstance().lookup(scheduleid);
				if (scheduler == null) {
					throw new GoException("Cannot find the Scheduler with ID : " + scheduleid + " . Check your config file is correct.");
				}
				plugin.initialize(id, scheduler);
				scheduler.getResource().addSchedulerPlugin(plugin);
			}
		} catch (XPathExpressionException e) {
			throw new GoException("instantiate plugins error : " + e.getMessage(), e);
		} catch (Exception e) {
			throw new GoException("instantiate plugins error : " + e.getMessage(), e);
		}
	}

	private Collection<Scheduler> instantiateRmiClient() {
		try {
			XPath xpath = XmlUtil.buildXpath();
			NodeList nodes = (NodeList) xpath.evaluate("/go/schedules/schedule", goConfigDoc, XPathConstants.NODESET);
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = nodes.item(i);
				String id = XmlUtil.getTrimmedToNullString(xpath, "id", node);
				String type = XmlUtil.getTrimmedToNullString(xpath, "type", node);
				String remoteid = XmlUtil.getTrimmedToNullString(xpath, "remoteid", node);
				String registryhost = XmlUtil.getTrimmedToNullString(xpath, "registryhost", node);
				int registryport = XmlUtil.getTrimmedToInt(xpath, "registryport", node, -1);
				if (type.equals("RMICLIENT")) {
					SchedulerResources res = new SchedulerResources();
					res.setName(id);
					Scheduler sched = new RMIClientScheduler(res, remoteid, registryhost, registryport);
					SchedulerRepository schedRep = SchedulerRepository.getInstance();
					schedRep.bind(sched);
				}
			}
		} catch (Exception e) {
			log.error("{}", e);
		}
		return null;
	}

	private Collection<Scheduler> instantiateRmiServer() {
		try {
			XPath xpath = XmlUtil.buildXpath();
			NodeList nodes = (NodeList) xpath.evaluate("/go/schedules/schedule", goConfigDoc, XPathConstants.NODESET);
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = nodes.item(i);
				String id = XmlUtil.getTrimmedToNullString(xpath, "id", node);
				String type = XmlUtil.getTrimmedToNullString(xpath, "type", node);
				//String poolid = XmlUtil.getTrimmedToNullString(xpath, "poolid", node);
				String registryhost = XmlUtil.getTrimmedToNullString(xpath, "registryhost", node);
				int registryport = XmlUtil.getTrimmedToInt(xpath, "registryport", node, -1);
				//String export = XmlUtil.getTrimmedToNullString(xpath, "export", node);
				if (type.equals("RMISERVER")) {
					SchedulerResources res = new SchedulerResources();
					res.setWorkerStore(new WorkStoreMemory());
					res.addGoPool(GoThreadPoolRepository.getInstance().lookup());
					res.setName(id);
					res.setRMIRegistryHost(registryhost);
					res.setRMIServerPort(registryport);
					res.setRMIRegistryPort(registryport);
					Scheduler sched = new RemoteScheduler(res);
					SchedulerRepository schedRep = SchedulerRepository.getInstance();
					schedRep.bind(sched);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	private void instantiateThreadPool() throws GoException {
		XPath xpath = XmlUtil.buildXpath();
		try {
			NodeList nodes = (NodeList) xpath.evaluate("/go/threadpools/threadpool", goConfigDoc, XPathConstants.NODESET);
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = nodes.item(i);
				int threadcount = XmlUtil.getTrimmedToInt(xpath, "threadcount", node, 0);
				String className = XmlUtil.getTrimmedToNullString(xpath, "class", node);
				String threadpoolId = XmlUtil.getTrimmedToNullString(xpath, "id", node);
				Class<GoThreadPool> pool = (Class<GoThreadPool>) Class.forName(className);
				GoThreadPool goThreadPool = pool.newInstance();
				//GoThreadPool theadpool = GoThreadPoolFactory.getGoThreadPoolInstance();
				goThreadPool.setThreadCount(threadcount);
				goThreadPool.initialize();
				GoThreadPoolRepository.getInstance().bind(goThreadPool);
			}
		} catch (XPathExpressionException e) {
			throw new GoException("Unable to instantiate class load helper class: " + e.getMessage(), e);
		} catch (Exception e) {
			throw new GoException("Unable to instantiate class load helper class: " + e.getMessage(), e);
		}
	}

	@SuppressWarnings("unused")
	private <T> Class<T> loadClass(String className, T k) throws ClassNotFoundException, GoException {
		try {
			ClassLoader cl = ClassUtils.findClassloader();
			if (cl != null) {
				Class<?> obj = cl.loadClass(className);
				return (Class<T>) obj;
			}
			throw new GoException("Unable to find a class loader on the current thread or class.");
		} catch (ClassNotFoundException e) {
			if (getClass().getClassLoader() != null)
				return (Class<T>) getClass().getClassLoader().loadClass(className);
			throw e;
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.go.SchedulerFactory#getAllSchedulers()
	 */
	@Override
	public Collection<Scheduler> getAllSchedulers() throws GoException {
		if (goConfigDoc == null) {
			return instantiate();
		} else {
			return null;
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.go.SchedulerFactory#getScheduler(java.lang.String)
	 */
	@Override
	public Scheduler getScheduler(String schedName) throws GoException {
		if (goConfigDoc == null) {
			try {
				initXmlFile();
			} catch (ValidationException e) {
				log.error("{}", e);
			} catch (SAXException e) {
				log.error("{}", e);
			} catch (IOException e) {
				log.error("{}", e);
			} catch (ClassNotFoundException e) {
				log.error("{}", e);
			} catch (ParseException e) {
				log.error("{}", e);
			} catch (XPathException e) {
				log.error("{}", e);
			} catch (Exception e) {
				log.error("{}", e);
			}
		}
		SchedulerRepository schedRep = SchedulerRepository.getInstance();
		Scheduler sched = schedRep.lookup(schedName);
		if (sched != null) {
			try {
				if (sched.isShutdown()) {
					schedRep.remove(getSchedulerName());
				} else {
					return sched;
				}
			} catch (SchedulerException e) {
				e.printStackTrace();
			}
		}
		if (sched == null) {
			throw new GoException("can not find the Scheduler named " + schedName);
		}
		return sched;
	}

	public void initJMX() throws GoException {
		XPath xpath = XmlUtil.buildXpath();
		try {
			NodeList nodes = (NodeList) xpath.evaluate("/go/schedules/schedule/jmx", goConfigDoc, XPathConstants.NODESET);
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = nodes.item(i);
				String type = XmlUtil.getTrimmedToNullString(xpath, "type", node);
				String jmxProxyClass = XmlUtil.getTrimmedToNullString(xpath, GoDomain.PROP_SCHED_JMX_PROXY_CLASS, node);
				String objectName = XmlUtil.getTrimmedToNullString(xpath, GoDomain.PROP_SCHED_JMX_OBJECT_NAME, node);
				int export = XmlUtil.getTrimmedToInt(xpath, GoDomain.PROP_SCHED_JMX_EXPORT, node, 0);
				if ("LOCAL".equals(String.valueOf(type).toUpperCase())) {
					//boolean jmxExport = cfg.getBooleanProperty(GoDomain.PROP_SCHED_JMX_EXPORT);
					//String jmxObjectName = cfg.getStringProperty(GoDomain.PROP_SCHED_JMX_OBJECT_NAME);
					//boolean jmxProxy = cfg.getBooleanProperty(GoDomain.PROP_SCHED_JMX_PROXY);
					//String jmxProxyClass = cfg.getStringProperty(GoDomain.PROP_SCHED_JMX_PROXY_CLASS);
					//if (jmxProxyClass == null) {
					//throw new GoException("No JMX Proxy Scheduler class provided");
					//}
					//if (jmxProxy) {
					//					if (jmxProxyClass == null) {
					//						throw new SchedulerConfigException("No JMX Proxy Scheduler class provided");
					//					}
					// Create class load helper
					//ClassLoadHelper loadHelper = null;
					//try {
					//String classLoadHelperClass = cfg.getStringProperty(GoDomain.PROP_SCHED_CLASS_LOAD_HELPER_CLASS, "org.go.spi.impl.CascadingClassLoadHelper");
					//loadHelper = (ClassLoadHelper) loadClass(classLoadHelperClass).newInstance();
					//} catch (Exception e) {
					//throw new Exception("Unable to instantiate class load helper class: " + e.getMessage(), e);
					//}
					//loadHelper.initialize();
					//RemoteMBeanScheduler jmxScheduler = null;
					//try {
					//jmxScheduler = (RemoteMBeanScheduler) loadHelper.loadClass(jmxProxyClass).newInstance();
					//} catch (Exception e) {
					//throw new GoException("Unable to instantiate RemoteMBeanScheduler class.", e);
					//}
					//if (jmxObjectName == null) {
					//String schedInstId = GoDomain.DEFAULT_INSTANCE_ID;
					//jmxObjectName = SchedulerResources.generateJMXObjectName(schedName, schedInstId);
					//}
					//jmxScheduler.setSchedulerObjectName(jmxScheduler);

					//Properties tProps = cfg.getPropertyGroup(GoDomain.PROP_SCHED_JMX_PROXY, true);

					//jmxScheduler.initialize();

					//schedRep.bind(jmxScheduler);
					//try {

					//SchedulerResources rsrcs = new SchedulerResources();
					//rsrcs.setName(schedName);
					//rsrcs.setThreadName(threadName);
					//rsrcs.setInstanceId(schedInstId);
					//rsrcs.setJobRunShellFactory(jrsf);
					//rsrcs.setMakeSchedulerThreadDaemon(makeSchedulerThreadDaemon);
					//rsrcs.setThreadsInheritInitializersClassLoadContext(threadsInheritInitalizersClassLoader);
					//rsrcs.setRunUpdateCheck(!skipUpdateCheck);
					//rsrcs.setBatchTimeWindow(batchTimeWindow);
					//rsrcs.setMaxBatchSize(maxBatchSize);
					//rsrcs.setInterruptJobsOnShutdown(interruptJobsOnShutdown);
					//rsrcs.setInterruptJobsOnShutdownWithWait(interruptJobsOnShutdownWithWait);
					//rsrcs.setJMXExport(true);
					//rsrcs.setJMXObjectName(jmxObjectName);
					//setBeanProps(jmxScheduler, tProps);
					registerJMX(export);
					break;
				}
			}
		} catch (Exception e) {
			//GoException initException = new GoException("RemoteMBeanScheduler class '" + "' props could not be configured.", e);
			throw new GoException(e);
		}
		//				SchedulerResources res = new SchedulerResources();
		//				res.setWorkerStore(new WorkStoreMemory());
		//				res.addGoPool(GoThreadPoolRepository.getInstance().lookup());
		//				Scheduler sched = new MemoryScheduler(res);
		//				res.setName(id);
		//				SchedulerRepository schedRep = SchedulerRepository.getInstance();
		//				schedRep.bind(sched);
		//	}
		//}
		//return jmxScheduler;
		//}
	}

	private java.lang.reflect.Method getSetMethod(String name, PropertyDescriptor[] props) {
		for (int i = 0; i < props.length; i++) {
			java.lang.reflect.Method wMeth = props[i].getWriteMethod();

			if (wMeth != null && wMeth.getName().equals(name)) {
				return wMeth;
			}
		}

		return null;
	}

	private void setBeanProps(Object obj, Properties props) throws NoSuchMethodException, IllegalAccessException, java.lang.reflect.InvocationTargetException, IntrospectionException, SchedulerConfigException {
		props.remove("class");

		BeanInfo bi = Introspector.getBeanInfo(obj.getClass());
		PropertyDescriptor[] propDescs = bi.getPropertyDescriptors();
		PropertiesParser pp = new PropertiesParser(props);

		java.util.Enumeration<Object> keys = props.keys();
		while (keys.hasMoreElements()) {
			String name = (String) keys.nextElement();
			String c = name.substring(0, 1).toUpperCase(Locale.US);
			String methName = "set" + c + name.substring(1);

			java.lang.reflect.Method setMeth = getSetMethod(methName, propDescs);

			try {
				if (setMeth == null) {
					throw new NoSuchMethodException("No setter for property '" + name + "'");
				}

				Class<?>[] params = setMeth.getParameterTypes();
				if (params.length != 1) {
					throw new NoSuchMethodException("No 1-argument setter for property '" + name + "'");
				}

				// does the property value reference another property's value? If so, swap to look at its value
				PropertiesParser refProps = pp;
				String refName = pp.getStringProperty(name);
				if (refName != null && refName.startsWith("$@")) {
					refName = refName.substring(2);
					refProps = cfg;
				} else
					refName = name;

				if (params[0].equals(int.class)) {
					setMeth.invoke(obj, new Object[] { Integer.valueOf(refProps.getIntProperty(refName)) });
				} else if (params[0].equals(long.class)) {
					setMeth.invoke(obj, new Object[] { Long.valueOf(refProps.getLongProperty(refName)) });
				} else if (params[0].equals(float.class)) {
					setMeth.invoke(obj, new Object[] { Float.valueOf(refProps.getFloatProperty(refName)) });
				} else if (params[0].equals(double.class)) {
					setMeth.invoke(obj, new Object[] { Double.valueOf(refProps.getDoubleProperty(refName)) });
				} else if (params[0].equals(boolean.class)) {
					setMeth.invoke(obj, new Object[] { Boolean.valueOf(refProps.getBooleanProperty(refName)) });
				} else if (params[0].equals(String.class)) {
					setMeth.invoke(obj, new Object[] { refProps.getStringProperty(refName) });
				} else {
					throw new NoSuchMethodException("No primitive-type setter for property '" + name + "'");
				}
			} catch (NumberFormatException nfe) {
				throw new SchedulerConfigException("Could not parse property '" + name + "' into correct data type: " + nfe.toString());
			}
		}
	}

	/**
	 * Register the scheduler in the local MBeanServer.
	 * @throws NullPointerException 
	 * @throws MalformedObjectNameException 
	 * @throws NotCompliantMBeanException 
	 * @throws MBeanRegistrationException 
	 * @throws InstanceAlreadyExistsException 
	 */
	private void registerJMX(int export) throws Exception {
		Map map = new HashMap();
		map.put("HelloAgent:name=helloWorld1", "org.go.ee.jmx.mbean.jvm.JVM5Monitor");
		RmiAgent rmiAgent = new RmiAgent(map, export);
		rmiAgent.start();
	}
}
